using System.Net;
using System.Net.Sockets;

using Common.NetWork;

namespace GRPCDemo.Network
{
    public class NetConnection<T> where T : INetSession
    {
        private SocketAsyncEventArgs eventArgs;

        public delegate void DataReceivedCallback(NetConnection<T> sender, DataEventArgs e);

        public delegate void DisconnectedCallback(NetConnection<T> sender, SocketAsyncEventArgs e);

        private T session;
        public T Session => session;

        public PackageHelper<NetConnection<T>> packageHelper;

        #region Internal Classes

        internal class State
        {
            public DataReceivedCallback? dataReceived;
            public DisconnectedCallback? disconnectedCallback;
            public Socket? socket;
        }

        #endregion

        public NetConnection(Socket socket, SocketAsyncEventArgs args, DataReceivedCallback dataReceived,
            DisconnectedCallback disconnectedCallback, T session)
        {
            lock (this)
            {
                packageHelper = new PackageHelper<NetConnection<T>>(this);
                State state = new State()
                {
                    socket = socket,
                    dataReceived = dataReceived,
                    disconnectedCallback = disconnectedCallback
                };
                eventArgs = new SocketAsyncEventArgs();
                eventArgs.AcceptSocket = socket;
                eventArgs.Completed += ReceivedCompleted;
                eventArgs.UserToken = state;
                eventArgs.SetBuffer(new byte[64 * 1024], 0, 64 * 1024);

                BeginReceive(eventArgs);
                this.session = session;
            }
        }

        private void BeginReceive(SocketAsyncEventArgs args)
        {
            lock (this)
            {
                State? state = args.UserToken as State;
                Socket? socket = state?.socket;
                if (socket != null && socket.Connected)
                {
                    args?.AcceptSocket?.ReceiveAsync(args);
                }
            }
        }

        private void ReceivedCompleted(object? sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred == 0)
            {
                CloseConnection(args); //Graceful disconnect
                return;
            }
            if (args.SocketError != SocketError.Success)
            {
                CloseConnection(args); //NOT graceful disconnect
                return;
            }


            State? state = args.UserToken as State;

            byte[] data = new byte[args.BytesTransferred];
            Array.Copy(args.Buffer ?? [], args.Offset, data, 0, data.Length);

            state?.dataReceived?.Invoke(this, new DataEventArgs()
            {
                RemoteEndPoint = args.RemoteEndPoint as IPEndPoint,
                Data = data,
                Offset = 0,
                Length = data.Length
            });

            BeginReceive(args);
        }

        private void CloseConnection(SocketAsyncEventArgs args)
        {
            State? state = args.UserToken as State;
            Socket? socket = state?.socket;
            try
            {
                socket?.Shutdown(SocketShutdown.Both);
            }
            catch { } // throws if client process has already closed
            socket?.Close();
            socket = null;

            args.Completed -= ReceivedCompleted; //MUST Remember This!
            state?.disconnectedCallback?.Invoke(this, args);
        }

        public void SendResponse()
        {
            var data = this.session.GetResponse();
            this.SendData(data, 0, data.Length);
        }

        public void SendData(byte[] data, int offset, int count)
        {
            lock (this)
            {
                State? state = eventArgs.UserToken as State;
                Socket? socket = state?.socket;

                if (socket != null && socket.Connected)
                    //socket.Send(data, offset, count, SocketFlags.None);
                    socket.BeginSend(data, 0, count, SocketFlags.None, new AsyncCallback(SendCallback), socket);
            }
        }

        private void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                int bytesSent = client.EndSend(ar);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}
